package com.atguigu.gmall.index.tools;

import com.sun.org.apache.xpath.internal.operations.Bool;
import org.checkerframework.checker.units.qual.A;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Component;

import java.util.Arrays;
import java.util.Timer;
import java.util.TimerTask;

/**
 * 可重入锁工具类
 *
 * @author Jia Fa
 * @create 2021-12-12 18:43
 */
@Component
@Scope("prototype")  // 多实例创建对象
public class DistributedLock {

    @Autowired
    private StringRedisTemplate redisTemplate;

    // 定时器对象
    private Timer timer;

    /**
     * 可重入上锁
     *
     * @param lockName 锁名称
     * @param uuid     锁的随机码
     * @param expire   锁的有效时间
     * @return 是否成功加锁
     */
    public Boolean tryLock(String lockName, String uuid, Integer expire) {
        /**1 判断Redis中是否存在该锁 不存在则设置锁与过期时间
         * 2 Redis中存在锁,判断当前锁是否是自己的, 如果是则将锁的计数位+1 同时续期有效时间(redis.call('hexists', KEYS[1], ARGV[1]) == 1)表示返回值为1,不是计数位为1 )
         * 3 以上接不满足则返回 0 否则返回 1
         */
        String script = "if(redis.call('exists', KEYS[1]) == 0 or redis.call('hexists', KEYS[1], ARGV[1]) == 1) " +
                "then " +
                "   redis.call('hincrby', KEYS[1], ARGV[1], 1) " +
                "   redis.call('expire', KEYS[1], ARGV[2]) " +
                "   return 1 " +
                "else " +
                "   return 0 " +
                "end";
        // 注意 Redis中参数只允许传string类型  否则报错
        Boolean flag = this.redisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class), Arrays.asList(lockName), uuid, expire.toString()/*注意,需要转为字符串*/);

        if (!flag) {
            // 自旋
            try {
                Thread.sleep(100);
                this.tryLock(lockName, uuid, expire);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }else {
            // 获取到锁后 如果任务时间过长则 为锁重新续期  注意 解锁时需要将定时器清空
            this.renewExpire(lockName, uuid, expire);
        }
        return true; // 上锁成功
    }

    /**
     * 可重入式解锁
     * @param lockName
     * @param uuid
     */
    public void unLock(String lockName, String uuid) {
        /**1 判断当前锁是否存在以及是否是自己的锁 返回nil表示不满足 结束程序
         * 2 再判断当前锁计数位-1后值是否为0 若为0 则删除该锁 此时返回值应为1
         * 3 以上均不满足 返回0 说明当前锁计数位>0 还有使用者
         */
        String script = "if(redis.call('hexists', KEYS[1], ARGV[1]) == 0) " +
                "then " +
                "   return nil " +
                "elseif(redis.call('HINCRBY', KEYS[1], ARGV[1], -1) == 0) " +
                "then " +
                "   return redis.call('del', KEYS[1]) " +
                "else " +
                "   return 0 " +
                "end";

        Long flag = this.redisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Arrays.asList(lockName), uuid);
        if (flag==null){
            // 说明不是自己的锁  判断1
            throw new RuntimeException("你在尝试开启别人的锁");
        }else if (flag==1){
            // 说明已删除该锁 需要清空电子狗
            timer.cancel();
        }


    }

    /**
     * 定时任务看门狗程序
     * @param lockName
     * @param uuid
     * @param expire
     */
    public void renewExpire(String lockName, String uuid, Integer expire){
        String script="if(redis.call('hexists',KEYS[1],ARGV[1])==1) " +
                "then " +
                "   return redis.call('expire',KEYS[1],ARGV[2]) " +
                "else " +
                "   return 0 " +
                "end";
        // 定时任务  每隔1/3设置时间续期1/3设置时间
        timer = new Timer();
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                redisTemplate.execute(new DefaultRedisScript<>(script,Boolean.class), Arrays.asList(lockName), uuid,expire.toString());
            }
        }, expire*1000/3,expire*1000/3);
    }
}
